package edu.xhu.id.myid;

import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import cn.hutool.core.util.IdUtil;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;

/**
 * Twitter的分布式自增ID算法snowflake
 */
@Component
@Data
public class BiliSnowflakeService {
    // 第 1 部分(41位)
    // 开始时间截 (2022-04-01)
    private final long startTime = 1648742400000L;

    // 第 2 部分(10位）
    // 机器id所占的位数
    private final long workerIdBits = 5L;

    // 数据标识id所占的位数
    private final long datacenterIdBits = 5L;

    // 第 3 部分（12位）
    // 序列在id中占的位数
    private final long sequenceBits = 12L;

    // -1L ^ (‐1L << 5) = 31
    // 支持的最大机器id, 结果是 31
    private final long maxWorkerId = ~(-1L << workerIdBits);

    // ‐1L ^ (‐1L << 5) = 31
    // 支持的最大数据标识id, 结果是 31
    private final long maxDatacenterId = ~(-1L << datacenterIdBits);

    // ‐1L ^ (‐1L << 12) = 4095
    // 自增长最大值4095, 0开始
    private final long sequenceMask = ~(-1L << sequenceBits);

    // 时间截向左移 22 位(5+5+12)
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

    // 数据标识id向左移 17 位(12+5)
    private final long datacenterIdShift = sequenceBits + workerIdBits;

    // 机器ID向左移 12 位
    private final long workerIdShift = sequenceBits;

    // 工作机器ID(0~31)
    private long workerId;

    // 数据中心ID(0~31)
    private long datacenterId;

    // 1毫秒内序号(0~4095)
    private long sequence = 0L;

    // 上次生成ID的时间截
    private long lastTimestamp = -1L;

    @Autowired
    private RedisTemplate<String, String> redisTemplate;


    //redis中的workerid锁
    private final String LOCK_KEY_PREFIX = "snowflake:workerid_";
    private final long LOCK_EXPIRE_SECONDS = 30L;
    private String LOCK_VALUE; //使用uuid来唯一标识该微服务

    public BiliSnowflakeService() {
        this.LOCK_VALUE = IdUtil.simpleUUID(); //使用uuid来唯一标识该微服务
    }

    // 获得下一个ID(该方法是线程安全的)
    public synchronized long nextId() {

        long timestamp = timeGen();

        // 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过
        // 这个时候应当抛出异常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        // 如果是同一时间生成的,则进行毫秒内序列
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            // 毫秒内序列溢出
            if (sequence == 0) {
                // 阻塞到下一个毫秒,获得新的时间戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            // 时间戳改变,毫秒内序列重置
            sequence = 0L;
        }

        // 上次生成ID的时间截
        lastTimestamp = timestamp;

        // 移位并通过或运算拼到一起组成64位的ID
        return ((timestamp - startTime) << timestampLeftShift) //时间戳左移22位
                | (datacenterId << datacenterIdShift) //数据标识左移17位
                | (workerId << workerIdShift) //机器id标识左移12位
                | sequence;
    }

    // 阻塞到下一个毫秒,直到获得新的时间戳
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    // 返回以毫秒为单位的当前时间
    protected long timeGen() {
        return System.currentTimeMillis();
    }

    //尝试锁上某个workerid
    @PostConstruct //该方法在 bean 构造后执行。
    public void tryLockWorkerId() throws InterruptedException {
        boolean locked = false;

        //轮询加锁
        while (!locked) {
            for (int i = 0; i < maxWorkerId; i++) {
                //尝试加锁，有效时间30s
                locked = redisTemplate.opsForValue().setIfAbsent(LOCK_KEY_PREFIX + i, LOCK_VALUE, LOCK_EXPIRE_SECONDS, TimeUnit.SECONDS).booleanValue();
                //加锁成功
                if (locked) {
                    this.workerId = i;
                    this.datacenterId = 0; //数据中心id默认设置为0
                    break;
                }
            }
            TimeUnit.MILLISECONDS.sleep(50);
        }

        System.out.println("tryLockWorkerId");
    }

    @Scheduled(fixedDelay = 10000) // 每隔十秒刷新一次锁的有效期
    public void refreshLock() {
        System.out.println("refreshLock");

        String luaScript =
                "if (redis.call('get',KEYS[1]) == ARGV[1]) then " +
                        "return redis.call('expire',KEYS[1],ARGV[2]) " +
                        "else " +
                        "return 0 " +
                        "end";
        boolean refreshed = redisTemplate.execute(new DefaultRedisScript<>(luaScript, Boolean.class),
                Arrays.asList(LOCK_KEY_PREFIX + this.workerId), LOCK_VALUE, String.valueOf(LOCK_EXPIRE_SECONDS)
        );

        //刷新失败
        if (!refreshed)
            throw new RuntimeException("Failed to refresh lock validity period");
        // redisTemplate.expire(LOCK_KEY_PREFIX + this.workerId, LOCK_EXPIRE_SECONDS, TimeUnit.SECONDS);
    }

    //释放workerid
    @PreDestroy //该方法在 bean 销毁前执行。
    public void unlock() {
        System.out.println("unlock");
        //判断加锁与解锁是不是同一个客户端，同一个才行，自己只能删除自己的锁，不误删他人的
        //将判断+删除自己的锁合并为lua脚本保证原子性
        String luaScript =
                "if (redis.call('get',KEYS[1]) == ARGV[1]) then " +
                        "return redis.call('del',KEYS[1]) " +
                        "else " +
                        "return 0 " +
                        "end";
        redisTemplate.execute(new DefaultRedisScript<>(luaScript, Boolean.class), Arrays.asList(LOCK_KEY_PREFIX + this.workerId), LOCK_VALUE);
    }
}
